﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using CardPlay.Model.ActionModel;
using CardPlay.Model.CardModel;
using CardPlay.Model.GameModel.Actions;
using CardPlay.Model.GameModel.Presentation;
using CardPlay.Model.Layout;
using CardPlay.Model.Primitives;

namespace CardPlay.Model.GameModel
{
    public abstract class CardGame : ICardGame, INotifyPropertyChanged
    {
        #region Standard part names

        public const string Stock = StandardPileNames.Stock;
        public const string Waste = StandardPileNames.Waste;
        public const string Reserve = StandardPileNames.Reserve;

        public const string AnyFoundation = StandardPileNames.PrefixForFoundation + "*";
        public const string AnyReserve = StandardPileNames.PrefixForReserve + "*";
        public const string AnyTableau = StandardPileNames.PrefixForTableau + "*";

        #endregion

        private readonly List<CardPile> _components = new List<CardPile>();
        private readonly ObservableCollection<PartLocation> _selection = new ObservableCollection<PartLocation>();
        private readonly ActionHistory _history;

        private int? _maximumDeals;
        private int _cardsPerDeal = 3;

        protected CardGame()
        {
            NameOfDealTarget = Waste;

            _history = new HistoryOfPlays(this) { HistoryChanged = NotifyOfStateChange };
            _history.TransactionEnding += delegate { OnTransactionEnding(); };
        }

        public event PropertyChangedEventHandler PropertyChanged = delegate { };
        public event EventHandler<StateOfPlayChangedEventArgs> StateOfPlayChanged = delegate { };
        public event EventHandler InitialDealCompleted = delegate { };
        public event EventHandler ActionExecuted = delegate { };

        public bool IsEnabled
        {
            get { return this.IsPlayable(); }
        }

        public StateOfPlay StateOfPlay
        {
            get
            {
                return
                    TotalMoves == 0 ?
                        StateOfPlay.NotStarted :
                    _history.InTransaction ?
                        StateOfPlay.InProgress :
                    IsComplete ?
                        StateOfPlay.GameComplete :
                    IsOver ?
                        StateOfPlay.GameOver :
                        StateOfPlay.InProgress;
            }
        }

        public IServiceProvider Host
        {
            get;
            set;
        }

        public IList<CardPile> CardPiles
        {
            get { return _components; }
        }
    
        public ObservableCollection<PartLocation> Selection
        {
            get { return _selection; }
        }

        public IPlayingRules Regulations
        {
            get; private set;
        }

        public IActionHistory History
        {
            get { return _history; }
        }

        public string NameOfDealTarget
        {
            get; set;
        }

        public int CardsPerDeal
        {
            get { return _cardsPerDeal; }
            protected set
            {
                Contract.Requires<ArgumentOutOfRangeException>(value > 0, "CardsPerDeal");
                _cardsPerDeal = value;
            }
        }

        public int TotalDeals
        {
            get;
            internal set;
        }

        public int TotalMoves
        {
            get { return _history.TotalChanges; }
        }

        public virtual bool IsRedealPossible
        {
            get { return MaximumDeals == null || TotalDeals < MaximumDeals; }
        }

        public virtual int PercentComplete
        {
            get { return CardsAtStart == 0 ? 0 : (CardsAt(AnyFoundation).Count() * 100 / CardsAtStart); }
        }

        public CardPile this[String pileName]
        {
            get { return _components.FirstOrDefault(pile => pile.Name == pileName); }
        }

        public virtual bool IsActionPossibleAt(PartLocation location)
        {
            return IsChangeSelectionAllowedAt(location) || AvailableActionsAt(location).Any();
        }

        public Card CardAt(PartLocation location)
        {
            var pile = location.Index < 0 ? null : PileAt(location);
            return (pile != null && location.Index < pile.Count) ? pile.CardAt(location.Index) : null;
        }

        public CardPile PileAt(PartLocation location)
        {
            return this[location.Path];
        }

        public CardPile FindPreviousCardPile(CardPile pile)
        {
            var index = CardPiles.IndexOf(pile);
            return index < 1 ? null : _components[index - 1];
        }

        public IEnumerable<CardPile> PilesNamed(IEnumerable<string> list)
        {
            return _components.Join(list, pile => pile.Name, name => name, (pile, name) => pile);
        }

        public IEnumerable<CardPile> PilesNamed(string pattern)
        {
            var expression = new Regex(pattern, RegexOptions.IgnoreCase);
            return _components.Where(pile => expression.IsMatch(pile.Name));
        }

        public void Deal(IRandomNumberSequence randomSeries)
        {
            InitializeComponents();
            Regulations = CreateRegulations();

            var deck = CardsInInitialDeal.ToList().Shuffle(randomSeries.Next);
            CardsAtStart = deck.Count;

            new Dealer(deck, (card, location) => this[location].AddCardToTop(card)).Deal(CreateDealScript());
            Guard.Against<InvalidOperationException>(deck.Any(), "deck not fully dealt");

            TotalDeals = 1;
            InitialDealCompleted(this, EventArgs.Empty);
        }

        public void Execute(IAction action)
        {
            if (PreprocessAction != null)
                action = PreprocessAction(action);

            using (var transaction = _history.TransactionScope)
            {
                transaction.Execute(action);
            }
        }

        public abstract ILayoutSite CreatePresentation(IPresentationContext presentation);

        public virtual RemovePileAction CreatePickupActionAt(PartLocation location)
        {
            return CardAt(location) != null ? new RemovePileAction(this, location) : null;
        }

        public virtual bool ExecuteCommandAt(PartLocation location, bool auto)
        {
            var action = auto ? QueryAutoActionAt(location) : QueryActionAt(location);
            
            if (action != null)
            {
                Execute(action);
                return true;
            }
            
            return false;
        }

        public PeekCardAction CreatePeekAction(PartLocation location)
        {
            return new PeekCardAction(this, location);
        }

        public MovePileAction CreateMovePileAction(RemovePileAction removeAction, PartLocation moveTo)
        {
            return new MovePileAction(this, removeAction, moveTo);
        }

        internal void NotifyOfPropertyChange(string property)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }

        internal void NotifyOfPropertyChange<TProperty>(Expression<Func<TProperty>> property)
        {
            var lambda = (LambdaExpression)property;

            var memberExpression = (lambda.Body is UnaryExpression) ?
                (MemberExpression)((UnaryExpression)lambda.Body).Operand :
                (MemberExpression)lambda.Body;

            NotifyOfPropertyChange(memberExpression.Member.Name);
        }

        #region Protected

        protected int? MaximumDeals
        {
            get { return _maximumDeals; }
            set
            {
                Contract.Requires<ArgumentException>(value == null || value >= 0, "RedealLimit");
                _maximumDeals = value;
            }
        }

        protected int CardsAtStart
        {
            get;
            private set;
        }

        protected IEnumerable<Card> CardsAt(string pattern)
        {
            return PilesNamed(pattern).SelectMany(pile => pile);
        }

        protected virtual bool IsComplete
        {
            get { return CardsAt(AnyFoundation).Count() == CardsAtStart; }
        }
        
        protected virtual bool IsOver
        {
            get { return false; }
        }

        protected Func<IAction, IAction> PreprocessAction { get; set; }

        protected abstract IEnumerable<String> InitialPileNames { get; }

        protected abstract IDealAction CreateDealScript();

        protected abstract IPlayingRules CreateRegulations();

        protected virtual void InitializeComponents()
        {
            _components.Clear();
            _components.AddRange(InitialPileNames.Select(name => new CardPile(name)));
        }

        protected virtual IEnumerable<Card> CardsInInitialDeal
        {
            get { return DeckFactory.CreateStandardDeck(); }
        }

        protected virtual bool IsChangeSelectionAllowedAt(PartLocation location)
        {
            return false;
        }

        protected virtual IAction QueryActionAt(PartLocation location)
        {
            GameAction action; 
            
            if (location.Path == Stock)
            {
                var waste = this[NameOfDealTarget];

                if (waste != null)
                {
                    if (location.Index >= 0 &&
                        Regulations.Permit(action = new DealAction(this, location, waste.Location)))
                        return action;
            
                    if (location.Index < 0 && ! waste.IsEmpty && 
                        Regulations.Permit(action = new ResetStockAction(this, new PartLocation(NameOfDealTarget, 0), location)))
                        return action;
                }
            }
            else if (CardAt(location) != null)
            {
                if (Regulations.Permit(action = new FlipCardAction(this, location)))
                    return action;

                if (Regulations.Permit(action = new RemoveCardAction(this, location)))
                    return action;
            }
            
            return null;
        }

        protected virtual IAction QueryAutoActionAt(PartLocation location)
        {
            var sourcePile = this[location.Path];
            
            if (sourcePile != null && ! sourcePile.IsEmpty && CardAt(location).IsFaceUp)
            {
                var pickupAction = CreatePickupActionAt(location);
                
                if (Regulations.Permit(pickupAction))
                {
                    return CardPiles.Where(pile => pile != sourcePile)
                        .Select(pile => new MovePileAction(this, pickupAction, pile.Top))
                        .FirstOrDefault(moveAction => Regulations.Permit(moveAction));
                }
            }
            
            return null;
        }

        protected virtual IEnumerable<IAction> AvailableActionsAt(PartLocation partLocation)
        {
            var pickupAction = CreatePickupActionAt(partLocation);
            if (pickupAction != null && Regulations.Permit(pickupAction)) yield return pickupAction;

            var action = QueryActionAt(partLocation);
            if (action != null) yield return action;
        }

        protected virtual void OnTransactionEnding()
        {
        }

        protected EmptyPileCue GetRedealBaseStyle()
        {
            return IsRedealPossible ? EmptyPileCue.Redeal : EmptyPileCue.NoRedeal;
        }

        protected VisualTraits CreateStockStyle(LayoutMetrics metrics)
        {
            var style = StandardStyles.Stock;
            style.MinimumSize = new Size(metrics.CardSize.Width * 5 / 4, metrics.CardSize.Height * 9 / 8);
            style.EmptyCue = GetRedealBaseStyle;

            return style;
        }

        #endregion

        private void NotifyOfStateChange(StateOfPlay oldState)
        {
            ActionExecuted(this, EventArgs.Empty);

            NotifyOfPropertyChange(() => TotalMoves);
            NotifyOfPropertyChange(() => PercentComplete);

            if (oldState == StateOfPlay) return;

            StateOfPlayChanged(this, new StateOfPlayChangedEventArgs(oldState, StateOfPlay));

            NotifyOfPropertyChange(() => IsComplete);
            NotifyOfPropertyChange(() => IsOver);
            NotifyOfPropertyChange(() => StateOfPlay);
            NotifyOfPropertyChange(() => IsEnabled);
        }
    }
}
